# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

import datetime
import functools
import logging
import sys

from coin.base.datetime_util import to_datetime
from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderSide
from coin.exchange.bitfinex_v2.kr_rest.product import (BitfinexCurrency, BitfinexProduct)
from coin.exchange.okex_futures.kr_rest.futures_product import (OkexFuturesProduct)
from coin.strategy.mm.bitfinex.simple_mm_executor import (BitfinexSimpleMMExecutor)
from coin.strategy.mm.base.bitfinex_strategy_base import (BitfinexMarginPositionBalancePrinter,
                                                          BitfinexStrategyBase,
                                                          run_live)
from coin.exchange.base.tick import BitfinexBTCUSDTickPrice
from coin.strategy.mm.base.dump.pass_unhedge import FillRow, FeatureRow
from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.mm.base.pass_unhedge import Pricer


class PassiveUnhedgeStrategy(BitfinexStrategyBase):
  def __init__(self, strategy_params, dry_run=True):
    trade_product = strategy_params['trade_product']
    super().__init__(products=[trade_product],
                     currencies=strategy_params['currencies'],
                     use_margin_account=strategy_params['use_margin_account'],
                     logger=logging.getLogger('PassiveUnhedgeStrategy'))

    self._strategy_params = strategy_params
    self._dry_run = dry_run

    self._trade_product = trade_product
    self._ref_product = strategy_params['ref_product']
    self._edge = strategy_params['edge_bp'] / 10000.

    self._bookmap = BookMap([self._trade_product, self._ref_product], logger=self._logger)
    self._pricer = Pricer(**strategy_params['pricing'])
    self._executor = None

    self._printer = BitfinexMarginPositionBalancePrinter(BitfinexCurrency.FromStrCurrency('USDT'),
                                                         self._trade_product,
                                                         self._bookmap,
                                                         self._logger)
    self._last_feature_dump_ts = 0

    # Print header
    self._logger.info(FillRow.get_header())
    self._logger.info(FeatureRow.get_header())

  def prepare(self):
    assert self.feed_subsystem is not None

    # Subscribe feed.
    for product in (self._trade_product, self._ref_product):
      self.feed_subsystem.subscribe_book(product, functools.partial(self.on_book, product))
    # Start balance and position printing.
    self._printer.start_print(self._ioloop, 30)

  def on_og_reset(self):
    self._printer.register_order_gateway(self.order_gateway)

    # Intialize passive executor
    self._logger.info('Intializing passive executor...')
    self._executor = BitfinexSimpleMMExecutor(self._strategy_params['executor'],
                                              self.order_gateway,
                                              self._logger)
    self.order_gateway.register_fill_callback(self._on_fill)

  def on_book(self, product, book):
    self._bookmap.set_book(product, book)

    # Check book freshness and sanity
    if not self._bookmap.is_book_ready() or not self._bookmap.is_book_sane():
      return

    self._pricer.update(self._bookmap.get_book(self._ref_product),
                        self._bookmap.get_book(self._trade_product))

    if not self._pricer.ready or not self.is_og_ready() or self._dry_run:
      return

    askp, bidp = self._pricer.calculate_price(self._edge, self._edge, True)
    self._executor.manage_mm_orders(askp, bidp)

    if book.timestamp - self._last_feature_dump_ts >= 2 * (10**9):
      self._last_feature_dump_ts = book.timestamp
      self._logger.info(self._pricer.last_value.to_log_string())

  def _on_fill(self, fill_price, fill_qty, order):
    timestamp = get_timestamp()

    pos, unrealized_pnl = self._printer._get_pos(self._trade_product)
    balance = self._og.get_balance(self._trade_product.quote)
    balance += unrealized_pnl

    if order.order_side == OrderSide.BUY:
      side_sign = 1
    elif order.order_side == OrderSide.SELL:
      side_sign = -1
    else:
      raise ValueError('Invalid order side: %s' % order.order_side)

    if order.internal.maker is not None:
      maker = order.internal.maker
    else:
      maker = -1

    pos += side_sign * fill_qty

    row = FillRow(time=timestamp,
                  time_hr=to_datetime(timestamp),
                  exchange='BitfinexMargin',
                  product=str(self._trade_product),
                  side=side_sign,
                  price=fill_price,
                  qty=fill_qty,
                  maker=maker,
                  fee=-1,
                  pos=pos,
                  balance=balance)
    self._logger.info(row.to_log_string())


def get_param_set(init_og_immediately=False):
  # Pricing
  trade_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_amt(10000.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_amt(10000.)[1][-1][0]))
  ref_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_qty(100.)[1][-1][0]),
                                 (lambda book: book.get_notional_bids_by_qty(100.)[1][-1][0]))

  # Execution
  trade_product = BitfinexProduct.FromStr('BTC-USD')
  basis_ma_window = 5
  lot_size = 0.01  # BTC
  stack = 5
  edge = 11.

  bitfinex_og_config = 'bitfinex_jshin00'
  use_margin_account = True

  NS_PER_MINUTE = 60 * (10**9)
  strategy_params = {
      'use_margin_account': use_margin_account,
      'currencies': [BitfinexCurrency.FromStrCurrency('USDT')],
      'trade_product': trade_product,
      'ref_product': OkexFuturesProduct.FromStr('BTC-USD.QUARTER'),
      'edge_bp': edge,
      'pricing': {
          'basis_ma_window': basis_ma_window * NS_PER_MINUTE,
          'tick': BitfinexBTCUSDTickPrice(),
          'book_askt_func_1': ref_product_true_book_funcs[0],
          'book_bidt_func_1': ref_product_true_book_funcs[1],
          'book_askt_func_2': trade_product_true_book_funcs[0],
          'book_bidt_func_2': trade_product_true_book_funcs[1]
      },
      'executor': {
          'product': trade_product,
          'lot_size': lot_size,
          'stickiness': 0.,
          'use_position': True,
          'min_pos': -lot_size * stack,
          'max_pos': lot_size * stack,
          'order_update_period': 2.,
      }
  }

  # Trading environment config
  if init_og_immediately:
    init_og_after_min = 0
  else:
    init_og_after_min = basis_ma_window + 0.1

  exit_after_min = init_og_after_min + 30
  env_config = {
      'og_config_name': bitfinex_og_config,
      'init_order_gateway_after': datetime.timedelta(minutes=init_og_after_min),
      'exit_after': datetime.timedelta(minutes=exit_after_min)
  }

  return strategy_params, env_config


def main(argv):
  strategy_params, env_config = get_param_set(init_og_immediately=False)

  # Run strategy!
  strategy = PassiveUnhedgeStrategy(strategy_params, dry_run=False)
  return run_live(strategy, **env_config)


if __name__ == '__main__':
  logging.basicConfig(level='DEBUG', format='%(levelname)8s %(asctime)s %(name)s] %(message)s')
  sys.exit(main(sys.argv))
